// Copyright 2006, FreeHEP.
package org.freehep.graphicsio.emf.gdiplus;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Properties;

import org.freehep.graphicsio.ImageGraphics2D;
import org.freehep.graphicsio.emf.EMFInputStream;
import org.freehep.graphicsio.emf.EMFOutputStream;

/**
 *  The Object metafile record contains information on an object 
 *  which must be stored for use by a future metafile record. 
 *  Examples include pens, brushes, fonts, images, StringFormat 
 *  objects, and so on. Naturally, the data format of each type 
 *  of object differs, and so the Flags value is used to determine 
 *  which type of object is being stored.
 *  <p>
 *  Object metafile records contain a numeric index (0 to 255). 
 *  The first Object record to appear in the file will have an 
 *  index of 0, the next record 1, and so on. Applications which 
 *  process metafiles should store these objects in an index-based 
 *  collection so that a later record can access them. For example, 
 *  a future DrawRects record might reference the Pen with index 2, 
 *  and the application should ensure that that object is still 
 *  accessible. Since the index is only one byte, only 256 objects 
 *  can be stored at any time. Thus, a simple stack will not suffice 
 *  and applications will have to be prepared for new Object records 
 *  overwriting old ones with the same index. 
 *  
 *  FIXME: no support for 16 bit
 *  
 * @author Mark Donszelmann
 * @version $Id: GDIPlusObject.java,v 1.1 2009-08-17 21:44:44 murkle Exp $
 */
public class GDIPlusObject extends EMFPlusTag {

	protected final static int INVALID = 0x000;
    protected final static int BRUSH = 0x100;
    protected final static int PEN = 0x200;
    protected final static int PATH = 0x300;
    protected final static int REGION = 0x400;
    protected final static int IMAGE = 0x500;
    protected final static int FONT = 0x600;
    protected final static int STRING_FORMAT = 0x700;
    protected final static int IMAGE_ATTRIBUTES = 0x800;
    protected final static int CUSTOM_LINE_CAP = 0x900;
	
    protected final static int BRUSH_TYPE_SOLID_COLOR       = 0;
    protected final static int BRUSH_TYPE_HATCH_FILL        = 1;
    protected final static int BRUSH_TYPE_TEXTURE_GRADIENT  = 2;
    protected final static int BRUSH_TYPE_PATH_GRADIENT     = 3;
    protected final static int BRUSH_TYPE_LINEAR_GRADIENT   = 4;
    
    protected final static int WRAP_MODE_TYLE = 0;
    protected final static int WRAP_MODE_TYLE_FLIP_X = 1;
    protected final static int WRAP_MODE_TYLE_FLIP_Y = 2;
    protected final static int WRAP_MODE_TYLE_FLIP_XY = 3;
    protected final static int WRAP_MODE_CLAMP = 4;
    
    protected final static int IMAGE_TYPE_UNKNOWN = 0;
    protected final static int IMAGE_TYPE_BITMAP = 1;
    protected final static int IMAGE_TYPE_METAFILE = 2;

    protected final static int FILL_MODE_ALTERNATE = 0x0000;
    protected final static int FILL_MODE_WINDING = 0x2000;
    
	private Paint brush;
    private BasicStroke stroke;
    private PathPoint[] path;
    private int pathFillMode;
    private RenderedImage image;
    
	public GDIPlusObject() {
		super(8,1);
	}
	
    public GDIPlusObject(int index, Paint brush) {
        this();
        this.brush = brush;
        flags = index | BRUSH; 
    }

    public GDIPlusObject(int index, Stroke stroke, Paint brush) {
        this();
        if (!(stroke instanceof BasicStroke)) throw new IllegalArgumentException(getClass()+": can only handle Stroke of class BasicStroke");
        this.stroke = (BasicStroke)stroke;
        this.brush = brush;
        flags = index | PEN;
    }
    
	public GDIPlusObject(int index, Shape shape, boolean windingFill) {
		this();
        try {
            EMFPlusPathConstructor p = new EMFPlusPathConstructor();
            p.reset();
            p.addPath(shape);
            path = p.getPath();
            flags = index | PATH; 
            pathFillMode = windingFill ? FILL_MODE_WINDING : FILL_MODE_ALTERNATE;
        } catch (IOException e) {
            // ignored
        }
	}
	
    public GDIPlusObject(int index, RenderedImage image) {
        this();
        this.image = image;
        flags = index | IMAGE;
    }
    
	public EMFPlusTag read(int tagID, int flags, EMFInputStream emf, int len)
			throws IOException {
		GDIPlusObject tag = new GDIPlusObject();
        tag.flags = flags;
        int type = flags & 0x0000FF00;
        // FIXME some missing
		switch(type) {
			case BRUSH:
			    tag.brush = readBrush(emf);
                break;
            case PEN:
                emf.readUINT();  // magic word
                emf.readUINT();  // unknown
                emf.readUINT();  // additional flags, NOTE no join, endcap, miterlimit etc.
                emf.readUINT();  // unknown
                float lineWidth = emf.readFLOAT();
                tag.stroke = new BasicStroke(lineWidth);
                tag.brush = readBrush(emf);
                break;                
			case PATH:
                emf.readUINT();  // magic word
                tag.path = new PathPoint[emf.readUINT()];
                int moreFlags = emf.readUINT();
                pathFillMode = moreFlags & 0x2000;
                for (int i=0; i<tag.path.length; i++) {
                    tag.path[i] = new PathPoint();
                    tag.path[i].setX(emf.readFLOAT());
                    tag.path[i].setY(emf.readFLOAT());
                }
                for (int i=0; i<tag.path.length; i++) {
                    tag.path[i].setType(emf.readUnsignedByte());
                }
                if (tag.path.length%4 > 0) {
                    for (int i=4-(tag.path.length%4); i>0; i--) {
                        emf.readBYTE();
                    }
                }
                break;
			case INVALID:
			default:
                System.err.println ("GDIObject: Invalid TYPE: "+Integer.toHexString(type));
				break;
		}
		return tag;
	}

	public void write(int tagID, int flags, EMFOutputStream emf) throws IOException {
		int type = flags & 0x0000FF00;
		switch(type) {
			case BRUSH:
                writeBrush(emf, brush);
				break;
            case PEN:
                emf.writeUINT(0xDBC01001);
                emf.writeUINT(0x0000);  // unknown
                emf.writeUINT(0x0000);  // additional flags, NOTE no join, endcap, miterlimit etc.
                emf.writeUINT(0x0000);  // unknown
                emf.writeFLOAT(stroke.getLineWidth());
                writeBrush(emf, brush);
                break;
			case PATH:
                emf.writeUINT(0xDBC01001);
				emf.writeUINT(path.length);
				emf.writeUINT(pathFillMode);
				for (int i=0; i<path.length; i++) {
					emf.writeFLOAT(path[i].getX());
					emf.writeFLOAT(path[i].getY());
				}
				for (int i=0; i<path.length; i++) {
					emf.writeUnsignedByte(path[i].getType());
				}
                if (path.length%4 > 0) {
                    for (int i=4-(path.length%4); i>0; i--) {
                        emf.writeBYTE(0);
                    }
                }
				break;
            case IMAGE:
                writeImage(emf, image);
                break;
			case INVALID:
			default:
				break;
		}		
	}
    
    public String toString() {
        StringBuffer sb = new StringBuffer(super.toString());
        sb.append("\n  ");
        int type = flags & 0x0000FF00;
        switch(type) {
            case BRUSH:
                sb.append("brush: "+brush);
                break;
            case PEN:
                sb.append("stroke: "+stroke);
                sb.append("\n  brush: "+brush);
                break;
            case PATH:
                sb.append("fillMode: "+pathFillMode);
                sb.append("\n  n: "+path.length);
                for (int i=0; i<path.length; i++) {
                    sb.append("\n  0x"+Integer.toHexString(path[i].getType())+" ("+path[i].getX()+", "+path[i].getY()+")");
                }
                break;
            default:
                sb.append("UNKNOWN");
                break;
        }
        return sb.toString();
    }
    
    private Paint readBrush(EMFInputStream emf) throws IOException {
        emf.readUINT();   // magic word
        int brushType = emf.readUINT();
        switch(brushType) {
            case BRUSH_TYPE_SOLID_COLOR:
                return emf.readCOLOR();
            case BRUSH_TYPE_LINEAR_GRADIENT:
                emf.readUINT(); // special mode ignored
                // FIXME, rest to be done
                return null;
/*                
            emf.writeUINT(paint.isCyclic() ? WRAP_MODE_TYLE_FLIP_XY : WRAP_MODE_CLAMP); // NOTE: check
            float x1 = (float)paint.getPoint1().getX();
            float y1 = (float)paint.getPoint1().getY();
            float x2 = (float)paint.getPoint2().getX();
            float y2 = (float)paint.getPoint2().getY();
            emf.writeFLOAT(Math.min(x1, x2));
            emf.writeFLOAT(Math.min(y1, y2));
            emf.writeFLOAT(Math.abs(x1-x2));
            emf.writeFLOAT(Math.abs(y1-y2));
            emf.writeCOLOR(paint.getColor1());
            emf.writeCOLOR(paint.getColor2());
            emf.writeCOLOR(paint.getColor1());
            emf.writeCOLOR(paint.getColor2());
        } else if (brush instanceof TexturePaint) {
//            emf.writeUINT(BRUSH_TYPE_TEXTURE_GRADIENT);
//FIXME later when image is done
            emf.writeUINT(BRUSH_TYPE_SOLID_COLOR);
            emf.writeCOLOR(Color.BLACK);                    
        } else {
            System.err.println("No Brush for paint of class: "+brush.getClass()+" defaulting to black");
            emf.writeUINT(BRUSH_TYPE_SOLID_COLOR);
            emf.writeCOLOR(Color.BLACK);
        }
*/
        }
        return Color.BLACK;
    }

    
    private void writeBrush(EMFOutputStream emf, Paint brush) throws IOException {
        emf.writeUINT(0xDBC01001);
        if (brush instanceof Color) {
            emf.writeUINT(BRUSH_TYPE_SOLID_COLOR);
            emf.writeCOLOR((Color)brush);
        } else if (brush instanceof GradientPaint) {
            GradientPaint paint = (GradientPaint)brush;
            emf.writeUINT(BRUSH_TYPE_LINEAR_GRADIENT);
            emf.writeUINT(0x02); // write Matrix
            emf.writeUINT(paint.isCyclic() ? WRAP_MODE_TYLE_FLIP_XY : WRAP_MODE_TYLE_FLIP_Y); // NOTE: check
            float x1 = (float)paint.getPoint1().getX();
            float y1 = (float)paint.getPoint1().getY();
            float x2 = (float)paint.getPoint2().getX();
            float y2 = (float)paint.getPoint2().getY();
            emf.writeFLOAT(Math.min(x1, x2));
            emf.writeFLOAT(Math.min(y1, y2));
            emf.writeFLOAT(Math.abs(x1-x2));
            emf.writeFLOAT(Math.abs(y1-y2));
            emf.writeCOLOR(paint.getColor1());
            emf.writeCOLOR(paint.getColor2());
            emf.writeCOLOR(paint.getColor1());
            emf.writeCOLOR(paint.getColor2());
            float dx = x2 - x1;
            float dy = y2 - y1;
            float scale = (float)paint.getPoint1().distance(paint.getPoint2())/dx;
            System.err.println(paint.getPoint1()+" "+paint.getPoint2());
            System.err.println(x1+" "+x2+" " +y1+" "+y2+":"+dx+" "+dy+" "+scale);
            float angle = (float)Math.atan2(dy, dx);
            AffineTransform transform = new AffineTransform(scale, 0, 0, scale, dx
                    / 2 + x1, dy / 2 + y1);
            transform = new AffineTransform();
            transform.scale(scale, scale);
            transform.rotate(angle);
            writeTransform(emf, transform);
        } else if (brush instanceof TexturePaint) {
            TexturePaint paint = (TexturePaint)brush;
            emf.writeUINT(BRUSH_TYPE_TEXTURE_GRADIENT);
            emf.writeUINT(0); // no special mode
            emf.writeUINT(WRAP_MODE_TYLE);
            writeImage(emf, paint.getImage());
        } else {
            System.err.println("No Brush for paint of class: "+brush.getClass()+" defaulting to black");
            emf.writeUINT(BRUSH_TYPE_SOLID_COLOR);
            emf.writeCOLOR(Color.BLACK);
        }
    }
    
    public static void writeTransform(EMFOutputStream emf, AffineTransform transform) throws IOException {
        emf.writeFLOAT((float)transform.getScaleX());
        emf.writeFLOAT((float)transform.getShearY()); 
        emf.writeFLOAT((float)transform.getShearX()); 
        emf.writeFLOAT((float)transform.getScaleY());
        emf.writeFLOAT((float)transform.getTranslateX());
        emf.writeFLOAT((float)transform.getTranslateY());
    }

    private void writeImage(EMFOutputStream emf, RenderedImage image) throws IOException {
        emf.writeUINT(0xDBC01001);
        emf.writeUINT(IMAGE_TYPE_BITMAP);
        emf.writeUINT(0);   // width
        emf.writeUINT(0);   // height
        emf.writeUINT(0);   // stride
        emf.writeUINT(0);   // pixelformat
        emf.writeUINT(0x000001);    // 01 00 00 00 for non-native images
        ImageGraphics2D.writeImage(image, "png", new Properties(), emf);
    }
}
